//
// File-system system calls.
// Mostly argument checking, since we don't trust
// user code, and calls into file.c and fs.c.
//

#include <fcntl.h>

#ifndef O_LARGEFILE
# warning "O_LARGEFILE is not defined."
# define O_LARGEFILE	0100000
#endif

#include "types.h"
#include "mmu.h"
#include "proc.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "string.h"
#include "console.h"
#include "log.h"
#include "fs.h"
#include "file.h"
#include "arm.h"
#include "syscall.h"

struct iovec {
    void  *iov_base;    /* Starting address. */
    size_t iov_len;     /* Number of bytes to transfer. */
};

static struct file *ofp(int fd) {
    struct file *f;
    assert(0 <= fd && fd < NOFILE);
    f = thisproc()->ofile[fd];
    assert(f != 0);
    return f;
}

/*
 * Fetch the nth word-sized system call argument as a file descriptor
 * and return both the descriptor and the corresponding struct file.
 */
static int
argfd(int n, int64_t *pfd, struct file **pf)
{
    int64_t fd;
    struct file *f;

    if (argint(n, (uint64_t*)&fd) < 0)
        return -1;
    if (fd < 0 || fd >= NOFILE || (f = thisproc()->ofile[fd]) == 0)
        return -1;
    if (pfd)
        *pfd = fd;
    if (pf)
        *pf = f;
    return 0;
}

/*
 * Allocate a file descriptor for the given file.
 * Takes over file reference from caller on success.
 */
static int
fdalloc(struct file *f)
{
    int fd = -1;
    struct proc *p = thisproc();

    for (int i = 0; i < NOFILE; i++) {
        if (!p->ofile[i]) {
            p->ofile[i] = f;
            fd = i;
            break;
        }
    }

    return fd;
}

// returns new file descriptor
static int sys_dup_impl(struct file *f) {
    int fd;
    if ((fd = fdalloc(f)) < 0)
        return -1;
    filedup(f);
    return fd;
}

int
sys_dup()
{
    struct file *f;

    if (argfd(0, 0, &f) < 0)
        return -1;
    return sys_dup_impl(f);
}

// returns the number of bytes read (zero indicates EOF)
static ssize_t sys_read_impl(struct file *f, char *buf, size_t count) {
    return fileread(f, buf, count);
}

ssize_t
sys_read()
{
    struct file *f;
    char *buf;
    size_t count;

    if (argfd(0, 0, &f) < 0 ||
        argint(2, &count) < 0 ||
        argptr(1, &buf, count) < 0)
        return -1;
    return sys_read_impl(f, buf, count);
}

// returns the number of bytes written
static ssize_t sys_write_impl(struct file *f, char *buf, size_t count) {
    return filewrite(f, buf, count);
}

ssize_t
sys_write()
{
    struct file *f;
    char *buf;
    size_t count;

    if (argfd(0, 0, &f) < 0 ||
        argint(2, &count) < 0 ||
        argptr(1, &buf, count) < 0)
        return -1;
    return sys_write_impl(f, buf, count);
}

ssize_t
sys_writev()
{
    struct file *f;
    int64_t fd, iovcnt;
    struct iovec *iov, *p;
    if (argfd(0, &fd, &f) < 0 ||
        argint(2, (uint64_t *)&iovcnt) < 0 ||
        argptr(1, (char**)&iov, iovcnt * sizeof(struct iovec)) < 0) {
        return -1;
    }

    size_t tot = 0;
    struct proc *proc = thisproc();
    for (p = iov; p < iov + iovcnt; p++) {
        if ((uint64_t)p->iov_base >= proc->sz || (uint64_t)p->iov_base + p->iov_len > proc->sz)
             return -1;
        ssize_t inc = filewrite(f, p->iov_base, p->iov_len);
        if (inc < 0)
            return -1;
        tot += inc;
    }
    return tot;
}

ssize_t sys_readv() {
    struct file *f;
    int64_t fd, iovcnt;
    struct iovec *iov, *p;
    if (argfd(0, &fd, &f) < 0 ||
        argint(2, (uint64_t *)&iovcnt) < 0 ||
        argptr(1, (char**)&iov, iovcnt * sizeof(struct iovec)) < 0) {
        return -1;
    }

    size_t tot = 0;
    struct proc *proc = thisproc();
    for (p = iov; p < iov + iovcnt; p++) {
        if ((uint64_t)p->iov_base >= proc->sz || (uint64_t)p->iov_base + p->iov_len > proc->sz)
             return -1;
        ssize_t inc = fileread(f, p->iov_base, p->iov_len);
        if (inc < 0)
            return -1;
        tot += inc;
    }
    return tot;
}

// returns zero on success
static int sys_close_impl(int fd, struct file *f) {
    thisproc()->ofile[fd] = 0;
    fileclose(f);
    return 0;
}

int
sys_close()
{
    int64_t fd;
    struct file *f;

    if (argfd(0, &fd, &f) < 0)
        return -1;
    return sys_close_impl(fd, f);
}

// returns zero on success
static int sys_fstat_impl(struct file *f, struct stat *st) {
    filestat(f, st);
    return 0;
}

int
sys_fstat()
{
    struct file *f;
    struct stat *st;

    if (argfd(0, 0, &f) < 0 ||
        argptr(1, (char**)&st, sizeof(struct stat)) < 0)
        return -1;

    return sys_fstat_impl(f, st);
}

static int sys_fstatat_impl(int64_t dirfd, char *path, struct stat *st, int64_t flags) {
    if (dirfd != AT_FDCWD) {
        cprintf("sys_fstatat: dirfd unimplemented\n");
        return -1;
    }
    if (flags != 0) {
        cprintf("sys_fstatat: flags unimplemented\n");
        return -1;
    }

    struct inode *ip;
    begin_op();
    if ((ip = namei(path)) == 0) {
        end_op();
        return -1;
    }
    ilock(ip);
    stati(ip, st);
    iunlockput(ip);
    end_op();

    return 0;
}

int
sys_fstatat()
{
    int64_t dirfd, flags;
    char *path;
    struct stat *st;

    if (argint(0, (uint64_t*)&dirfd) < 0 ||
        argstr(1, &path) < 0 ||
        argptr(2, (void *)&st, sizeof(*st)) < 0 ||
        argint(3, (uint64_t*)&flags) < 0)
        return -1;

    return sys_fstatat_impl(dirfd, path, st, flags);
}

// must be wrapped in a transaction
static struct inode *
create(char *path, short type, short major, short minor)
{
    struct inode *ip, *dp;
    char name[DIRSIZ];

    dp = nameiparent(path, name);
    if (!dp)
        return 0;

    ilock(dp);

    ip = dirlookup(dp, name, 0);
    if (ip) {
        iunlock(dp);
        ilock(ip);

        if (type == T_FILE && (ip->type == T_FILE || ip->type == T_DEV))
            return ip;

        iunlockput(ip);
        return 0;
    }

    // now ip = 0
    ip = ialloc(dp->dev, type);
    if (!ip)
        panic("create: failed to alloc inode for ip.");

    ilock(ip);

    ip->major = major;
    ip->minor = minor;
    ip->nlink = 1;
    iupdate(ip);

    if (type == T_DIR) {
        dp->nlink += 1;
        iupdate(dp);

        if (dirlink(ip, ".", ip->inum) < 0 ||
            dirlink(ip, "..", dp->inum) < 0)
            panic("create: failed to link \".\" or \"..\".");
    }

    if (dirlink(dp, name, ip->inum) < 0)
        panic("create: failed to link to parent.");

    iunlockput(dp);
    return ip;  // ip is still locked.
}

static int sys_openat_impl(int64_t dirfd, char *path, int64_t omode) {
    struct file *f;
    struct inode *ip;
    int64_t fd;

    if (dirfd != AT_FDCWD) {
        cprintf("sys_openat: dirfd unimplemented\n");
        return -1;
    }
    if ((omode & O_LARGEFILE) == 0) {
        cprintf("sys_openat: expect O_LARGEFILE in open flags\n");
        return -1;
    }

    begin_op();
    if (omode & O_CREAT) {
        // FIXME: Support acl mode.
        ip = create(path, T_FILE, 0, 0);
        if (ip == 0) {
            end_op();
            return -1;
        }
    } else {
        if ((ip = namei(path)) == 0) {
            end_op();
            return -1;
        }
        ilock(ip);
        if (ip->type == T_DIR && omode != (O_RDONLY | O_LARGEFILE)) {
            iunlockput(ip);
            end_op();
            return -1;
        }
    }

    if ((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0) {
        if (f)
            fileclose(f);
        iunlockput(ip);
        end_op();
        return -1;
    }
    iunlock(ip);
    end_op();

    f->type = FD_INODE;
    f->ip = ip;
    f->off = 0;
    f->readable = !(omode & O_WRONLY);
    f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
    return fd;
}

int
sys_openat()
{
    char *path;
    int64_t dirfd, omode;

    if (argint(0, (uint64_t*)&dirfd) < 0 ||
        argstr(1, &path) < 0 ||
        argint(2, (uint64_t*)&omode) < 0)
        return -1;

    return sys_openat_impl(dirfd, path, omode);
}

static int sys_mkdirat_impl(int64_t dirfd, char *path, int64_t mode) {
    struct inode *ip;

    if (dirfd != AT_FDCWD) {
        cprintf("sys_mkdirat: dirfd unimplemented\n");
        return -1;
    }
    if (mode != 0) {
        cprintf("sys_mkdirat: mode unimplemented\n");
        return -1;
    }

    begin_op();
    if ((ip = create(path, T_DIR, 0, 0)) == 0) {
        end_op();
        return -1;
    }
    iunlockput(ip);
    end_op();
    return 0;
}

int
sys_mkdirat()
{
    int64_t dirfd, mode;
    char *path;

    if (argint(0, (uint64_t*)&dirfd) < 0 ||
        argstr(1, &path) < 0 ||
        argint(2, (uint64_t*)&mode) < 0)
        return -1;
    return sys_mkdirat_impl(dirfd, path, mode);
}

static int sys_mknodat_impl(int64_t dirfd, char *path, int64_t major, int64_t minor) {
    struct inode *ip;

    if (dirfd != AT_FDCWD) {
        cprintf("sys_mknodat: dirfd unimplemented\n");
        return -1;
    }
    cprintf("mknodat: path '%s', major:minor %d:%d\n", path, major, minor);

    begin_op();
    if((ip = create(path, T_DEV, major, minor)) == 0) {
        end_op();
        return -1;
    }
    iunlockput(ip);
    end_op();
    return 0;
}

int
sys_mknodat()
{
    char *path;
    int64_t dirfd, major, minor;

    if (argint(0, (uint64_t*)&dirfd) < 0 ||
        argstr(1, &path) < 0 ||
        argint(2, (uint64_t*)&major) < 0 ||
        argint(3, (uint64_t*)&minor) < 0)
        return -1;

    return sys_mknodat_impl(dirfd, path, major, minor);
}

static int sys_chdir_impl(char *path) {
    struct inode *ip;
    struct proc *curproc = thisproc();

    begin_op();
    if ((ip = namei(path)) == 0) {
        end_op();
        return -1;
    }
    ilock(ip);
    if (ip->type != T_DIR) {
        iunlockput(ip);
        end_op();
        return -1;
    }
    iunlock(ip);
    iput(curproc->cwd);
    end_op();
    curproc->cwd = ip;
    return 0;
}

int
sys_chdir()
{
    char *path;

    if (argstr(0, &path) < 0)
        return -1;

    return sys_chdir_impl(path);
}

static int sys_exec_impl(char *path, int argc, char *argv[]) {
    // cprintf("cpu %d: sys_exec: executing \"%s\".\n", arm_cpuid(), path);
    // for (int j = 1; j < argc; j++) {
    //     cprintf("cpu %d: sys_exec: argument: \"%s\"\n", arm_cpuid(), argv[j]);
    // }

    return execve(path, argc, argv, 0, 0);
}

int
sys_exec()
{
    char *path, *argv[MAXARG];
    int i;
    uint64_t uargv, uarg;

    if(argstr(0, &path) < 0 || argint(1, &uargv) < 0){
        return -1;
    }
    memset(argv, 0, sizeof(argv));
    for(i = 0;; i++){
        if(i >= NELEM(argv)) {
            return -1;
        }
        if(fetchint(uargv + 8 * i, (int64_t*)&uarg) < 0) {
            return -1;
        }
        if(uarg == 0){
            argv[i] = 0;
            break;
        }
        if(fetchstr(uarg, &argv[i]) < 0) {
            return -1;
        }
    }

    return sys_exec_impl(path, i, argv);
}

/**
 * testbench for filesystem
 */

typedef struct {
    int fd;
    int valid;
    struct dirent entry;
} diter_t;

static ssize_t dir_next(diter_t *it) {
    ssize_t r = sys_read_impl(ofp(it->fd), (char*)&it->entry, sizeof(struct dirent));
    assert(r == 0 || r == sizeof(struct dirent));
    it->valid = r > 0;
    return r;
}

static int dir_begin(char *path, diter_t *it) {
    it->fd = sys_openat_impl(AT_FDCWD, path, O_RDONLY | O_LARGEFILE);
    assert(it->fd >= 0);

    struct stat st;
    assert(sys_fstat_impl(ofp(it->fd), &st) == 0);
    assert(st.st_mode == S_IFDIR);

    dir_next(it);
    return 0;
}

static int dir_end(diter_t *it) {
    return it->fd < 0 || !it->valid;
}

static void dir_close(diter_t *it) {
    assert(sys_close_impl(it->fd, ofp(it->fd)) == 0);
}

static char *ROOT_DIR = "/";
static char *CUR_DIR = "./";
static char *JUNK_DIR = "./junk";

void list_files(char *path) {
    cprintf("fs_test: \"ls %s\"\n", path);


    diter_t it;
    assert(dir_begin(path, &it) == 0);

    sys_chdir_impl(path);
    while (!dir_end(&it)) {
        struct dirent *e = &it.entry;
        if (e->inum) {
            size_t fsize = 0;

            if (namecmp(e->name, ".") != 0 && namecmp(e->name, "..") != 0) {
                struct stat st;
                assert(sys_fstatat_impl(AT_FDCWD, e->name, &st, 0) == 0);
                fsize = st.st_size;
            }

            cprintf("- \"%s/%s\": size=%u\n", path, e->name, fsize);
        }

        dir_next(&it);
    }
    sys_chdir_impl(ROOT_DIR);

    dir_close(&it);
}

static void fs_junk_test(int n_times) {
    if (sys_fstatat_impl(AT_FDCWD, "junk", 0, 0) < 0)
        assert(sys_mkdirat_impl(AT_FDCWD, "junk", 0) == 0);

    cprintf("fs_test: write/read test on \"./junk/cookie\".\n");
    int fw = sys_openat_impl(AT_FDCWD, "./junk/cookie", O_CREAT | O_LARGEFILE | O_WRONLY);
    assert(fw == 1);

    char *word = "元宵快乐";
    int len = strlen(word);
    for (int i = 0; i < n_times; i++) {
        ssize_t wcnt = sys_write_impl(ofp(fw), word, len);
        assert(wcnt == len);
    }

    list_files(JUNK_DIR);

    // ofp(fw)->off = 0;
    sys_close_impl(fw, ofp(fw));
    fw = sys_openat_impl(AT_FDCWD, "./junk/cookie", O_CREAT | O_LARGEFILE | O_RDWR);
    assert(fw == 1);

    cprintf("=./junk/cookie==========\n");
    static char buf[64];
    for (int i = 0; i < n_times; i++) {
        assert(sys_read_impl(ofp(fw), buf, len) == len);
        buf[len] = 0;
        assert(strcmp(word, buf) == 0);
        cprintf("%s", buf);
    }
    assert(sys_read_impl(ofp(fw), buf, len) == 0);
    cprintf("\n========================\n");

    sys_close_impl(fw, ofp(fw));
}

void fs_test() {
    cprintf("fs_test: begin\n");

    list_files(CUR_DIR);

    static char readme_buf[1024];
    cprintf("fs_test: \"cat /README\"\n");
    int freadme = sys_openat_impl(AT_FDCWD, "README", O_RDONLY | O_LARGEFILE);
    assert(freadme == 0);  // since we have closed it before
    assert(sys_read_impl(ofp(freadme), readme_buf, sizeof(readme_buf)) > 0);
    cprintf("=./README===============\n%s\n========================\n", readme_buf);

    fs_junk_test(150);
    fs_junk_test(150);

    sys_close_impl(freadme, ofp(freadme));
}
